package icasue.stronger.factory;

import icasue.base.touch.ApplicationTouch;
import icasue.stronger.wareable.logicstronger.StrongerAdvice;
import icasue.stronger.annotation.RuntimeReference;
import icasue.stronger.register.StrongerProxyBuffer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.InvocationHandler;
import icasue.reflect.handles.classes.ClassOF;
import icasue.reflect.handles.predicate.SureOF;

import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/6/29 下午3:50
 * @UpdateDate:
 * @Description: Core Proxy class, defined as a FactoryBean,
 * resolve runtime reference, create proxy instance, invoke stronger logic chain.
 */
@Slf4j
public class ProxyStronger<T,R extends T>  implements FactoryBean<T> {

    public Class<T> sourceClass;
    public R proxyRefObject;
    public Object realInvokeObject;
    public StrongerAdvice strongerAdvice;
    public String componentName;
    private static final MethodHandles.Lookup lookup = MethodHandles.lookup();
    private static final List<String> ignoreMethods = Arrays.asList("toString","hashCode","equals");

    public ProxyStronger(){ }

    public ProxyStronger(Class<T> sourceClass){
        this.sourceClass = sourceClass;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    /**
     * choose register suitable proxy,and try reference instance which application service needed.
     * @return
     */
    @Override
    public T getObject() {
        Optional<T> alreadyExistProxy = this.getFromProxyBuffer(this.sourceClass);
        if(alreadyExistProxy.isPresent()){
            return alreadyExistProxy.get();
        }else {
            //get real invoke,came form beanDefinition's reference or newInstance with reference.
            Object sourceMateDateInvoker = this.proxyRefObject == null ?
                    this.attendReferenceInstance(this.sourceClass,null) : this.proxyRefObject;
            this.realInvokeObject = sourceMateDateInvoker;
            //retain proxy's ref tree.
            ApplicationTouch.strongerProxyOriginRef.put(this.sourceClass,this.realInvokeObject);

            T genericProxy = null;
            boolean isOrHaveInterface = sourceClass.getInterfaces().length > 0 || sourceClass.isInterface();

            if(sourceClass.isInterface()){
                //Create proxy by JDK when sourceClass is interface.
                genericProxy = (T) Proxy.newProxyInstance(
                        sourceClass.getClassLoader(),
                        sourceClass.isInterface() ? new Class[]{ sourceClass } : sourceClass.getInterfaces(),
                        (proxy,method,args) -> {
                            return executeInvoke(sourceClass,realInvokeObject,method,args);
                        }
                );
            } else {
                //Create proxy by CGLIB when has no interface.
                Enhancer enhancer = new Enhancer();
                enhancer.setClassLoader(sourceClass.getClassLoader());
                enhancer.setSuperclass(sourceClass);
                enhancer.setCallback(new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
                        return executeInvoke(sourceClass,realInvokeObject,method,objects);
                    }
                });
                genericProxy = (T)enhancer.create();
            }
            this.putToProxyBuffer(this.sourceClass,genericProxy,isOrHaveInterface);
            //invoke this proxy object once directly, confirm not lazy loading.
            this.getFromProxyBuffer(this.sourceClass);
            //retain this proxy's ref instance generic by application. by this we can using reflect to access fields.
            ApplicationTouch.strongerProxyOriginRef.put(this.sourceClass,realInvokeObject);
            return genericProxy;
        }
    }

    /**
     * execute method invoke and deal stronger logic.
     * @param sourceMateDateInvoker
     * @param method
     * @param args
     * @return
     * @throws Exception
     */
    private Object executeInvoke(Class<T> sourceClass, Object sourceMateDateInvoker, Method method, Object... args) throws Throwable {
        boolean hasAdviceLogic = strongerAdvice == null ? false : true;
        Object result = null;
        if(ignoreMethods.contains(method.getName())){
            result = method.invoke(sourceMateDateInvoker, args);
        }else {
            if(hasAdviceLogic) {
                strongerAdvice.executeBeforeMethod(sourceClass, sourceMateDateInvoker, method, args);
            }
            try {
                result = method.invoke(sourceMateDateInvoker, args);
            }catch (InvocationTargetException e){
                if(hasAdviceLogic){
                    strongerAdvice.executeExceptionMethodHandles(e.getTargetException(), result, sourceClass, sourceMateDateInvoker, method, args);
                }
            }finally {
                if(hasAdviceLogic) {
                    strongerAdvice.executeAfterMethod(result, sourceClass, sourceMateDateInvoker, method, args);
                }
            }
        }
        return result;
    }

    /**
     * attend reference instance to target proxy, keep in application service's features and function.
     * @param sourceClass
     * @param existInstance
     * @return
     */
    public Object attendReferenceInstance(Class<?> sourceClass,Object existInstance){
        Object inventedProxyRefObject = existInstance;
        if(inventedProxyRefObject == null) {
            if(sourceClass.isInterface()) {
                throw new RuntimeException("Interface : " +sourceClass+ " cant be instance. ");
            }
            try {
                inventedProxyRefObject = BeanUtils.instantiateClass(sourceClass);
            } catch (Exception e) {
                throw new RuntimeException("Class : " + sourceClass + " has not PrimaryConstruct.");
            }
        }
        //try setValue to those fields which need to inject,include declaredField and super's public field.
        List<Field> trySetFieldList = new ArrayList<>();
        trySetFieldList.addAll(Arrays.asList(sourceClass.getDeclaredFields()));
        trySetFieldList.addAll(Arrays.asList(sourceClass.getFields()));
        List<Field> hasCandidateComponent = trySetFieldList.stream().distinct()
                .filter((field) -> {
                    for (Class<? extends Annotation> markAnnotation : StrongerProxyBuffer.referenceMarkAnnotations) {
                        Optional<? extends Annotation> intentionCandidate =
                                Optional.ofNullable(field.getDeclaredAnnotation(markAnnotation));
                        if (intentionCandidate.isPresent()) {
                            return true;
                        }
                    }
                    return false;
                }).collect(Collectors.toList());

        if(hasCandidateComponent == null || hasCandidateComponent.size() <= 0){
            return inventedProxyRefObject;
        }else {
            //try reference candidate component which has been proxied or exist in spring's BeanFactory.
            for (Field field : hasCandidateComponent) {
                Class<?> attendReferenceField = field.getType();
                Object existVirtualReference = StrongerProxyBuffer.proxyBuffer.get(attendReferenceField);
                if(existVirtualReference == null){
                    try {
                        existVirtualReference = StrongerProxyBuffer.beanFactory.getBean(attendReferenceField);
                    }catch (Exception e){
                        //When can't reference instance directly, maybe current depend exist multi implements,
                        //as the same time,try parse reference by component's name.
                        Boolean gettingComponentPass = Boolean.FALSE;
                        String componentName = null;
                        Optional<Autowired> autowiredOptional = Optional.ofNullable(attendReferenceField.getAnnotation(Autowired.class));
                        Optional<Resource> resourceOptional = Optional.ofNullable(attendReferenceField.getAnnotation(Resource.class));
                        Optional<RuntimeReference> runtimeReferenceOptional = Optional.ofNullable(attendReferenceField.getAnnotation(RuntimeReference.class));
                        if(autowiredOptional.isPresent()){
                            gettingComponentPass = Boolean.FALSE;
                        }else if(resourceOptional.isPresent()){
                            componentName = resourceOptional.get().name();
                        }else if(runtimeReferenceOptional.isPresent()){
                            componentName = runtimeReferenceOptional.get().name();
                        }else {
                            gettingComponentPass = Boolean.FALSE;
                        }
                        if(componentName != null && !"".equals(componentName.trim())){
                            existVirtualReference = Optional.ofNullable(StrongerProxyBuffer.proxyComponentNameMappingBuffer.get(componentName))
                                    .orElse(StrongerProxyBuffer.beanFactory.getBean(componentName));
                            if(existVirtualReference == null){
                                gettingComponentPass = Boolean.FALSE;
                            }else {
                                gettingComponentPass = Boolean.TRUE;
                            }
                        }
                        if(!gettingComponentPass) {
                            throw new RuntimeException("Bean " + sourceClass + "'s reference in class : "
                                    + attendReferenceField + " is not present, inventedProxyRefObject's create will been skip. ");
                        }
                    }
                }
                if(existVirtualReference != null){
                    try {
                        field.setAccessible(true);
                        field.set(inventedProxyRefObject,existVirtualReference);
                    } catch (Exception e) {
                        throw new RuntimeException("Occur an unKnow exception when reference " +attendReferenceField+ " on " + sourceClass);
                    }
                }else {
                    throw new RuntimeException("Bean " + sourceClass+ "'s reference in class : "
                            + attendReferenceField+ " is not present, inventedProxyRefObject's create will been skip. ");
                }
            }
            return inventedProxyRefObject;
        }
    }

    /**
     * postConstruct method invoke,register logic stronger normally.
     * @param strongerAdvice
     */
    public void postConstructStrongerAdviceInvoke(Object strongerAdvice){
        try {
            Method $postConstruct = ClassOF.getDeclaredMethod_.apply(StrongerAdvice.class, "postConstruct", null);
            $postConstruct.setAccessible(true);
            $postConstruct.invoke(strongerAdvice);
        } catch (Throwable e) {
           log.info("StrongerAdvice : {} ,invoke method postConstruct occur an exception. ",strongerAdvice.getClass());
        }
    }

    /**
     * Default store class,or his fist interface's proxy.
     * @param sourceClass
     * @param genericProxy
     * @param isOrHaveInterface
     */
    public void putToProxyBuffer(Class<T> sourceClass,T genericProxy,boolean isOrHaveInterface){
        StrongerProxyBuffer.proxyBuffer.put(sourceClass,genericProxy);
        StrongerProxyBuffer.proxyComponentNameMappingBuffer.put(this.componentName,genericProxy);
        if(isOrHaveInterface){
            if(!sourceClass.isInterface()){
                //Current direct Interface exist many implements component,
                //reference need using ActualImplement, or given component's name.
                //on this case, interface's someones reProxy mapping has lessMeaning. ignore storing.
                if(StrongerProxyBuffer.proxyBuffer.get(sourceClass.getInterfaces()[0]) != null) {
                    StrongerProxyBuffer.proxyBuffer.remove(sourceClass.getInterfaces()[0]);
                }else {
                    StrongerProxyBuffer.proxyBuffer.put(sourceClass.getInterfaces()[0], genericProxy);
                }
            }
        }
    }

    /**
     * Get suitable proxy from ProxyBucket.
     * @param sourceClass
     * @return
     */
    public Optional<T> getFromProxyBuffer(Class<T> sourceClass){
        //Here if ProxyBucket has this sourceClass's proxy,optional is present.
        Optional<T> optional = Optional.ofNullable((T)StrongerProxyBuffer.proxyBuffer.get(sourceClass));
        return optional;
    }

    @Override
    public Class<T> getObjectType() {
        return sourceClass;
    }

    public R getProxyRefObject() {
        return proxyRefObject;
    }

    public void setProxyRefObject(R proxyRefObject) {
        this.proxyRefObject = proxyRefObject;
    }

    public StrongerAdvice getStrongerAdvice() {
        return strongerAdvice;
    }

    public void setStrongerAdvice(StrongerAdvice strongerAdvice) {
        SureOF.notNull(strongerAdvice,"Stronger advice Must be null, suggest given a ref or instance.");
        this.attendReferenceInstance(strongerAdvice.getClass(), strongerAdvice);
        this.postConstructStrongerAdviceInvoke(strongerAdvice);
        this.strongerAdvice = strongerAdvice;
    }

    public String getComponentName() {
        return componentName;
    }

    public void setComponentName(String componentName) {
        this.componentName = componentName;
    }
}